package com.yanyeori.framework.jira.jiraclient;

import net.sf.json.JSON;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import org.apache.http.client.HttpClient;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.ssl.TrustStrategy;

import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * A simple JIRA REST client.
 */
public class JiraClient {

    private RestClient restclient;
    private String username = null;

    /**
     * Creates a JIRA client.
     *
     * @param uri Base URI of the JIRA server
     * @throws JiraException
     */
    public JiraClient(String uri) throws JiraException {
        this(null, uri, null);
    }

    /**
     * Creates an authenticated JIRA client.
     *
     * @param uri   Base URI of the JIRA server
     * @param creds Credentials to authenticate with
     * @throws JiraException
     */
    public JiraClient(String uri, ICredentials creds) throws JiraException {
        this(null, uri, creds);
    }

    /**
     * Creates an authenticated JIRA client with custom HttpClient.
     *
     * @param httpClient Custom HttpClient to be used
     * @param uri        Base URI of the JIRA server
     * @param creds      Credentials to authenticate with
     * @throws JiraException
     */
    public JiraClient(HttpClient httpClient, String uri, ICredentials creds) throws JiraException {
        if (httpClient == null) {
            httpClient = HttpClientBuilder.create()
                    .setMaxConnTotal(50)
                    .setMaxConnPerRoute(50)
                    .setSSLSocketFactory(getSslConnectionSocketFactory())
                    .build();
        }

        restclient = new RestClient(httpClient, creds, URI.create(uri));

        if (creds != null) {
            username = creds.getLogonName();
            //intialize connection if required
            creds.initialize(restclient);
        }
    }

    /**
     * 支持SSL
     *
     * @return SSLConnectionSocketFactory
     */
    private static SSLConnectionSocketFactory getSslConnectionSocketFactory() {
        try {
            //忽略安全证书配置
            TrustStrategy acceptingTrustStrategy = (x509Certificates, s) -> true;
            SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build();
            return new SSLConnectionSocketFactory(sslContext, new NoopHostnameVerifier());
        } catch (Exception e) {
            throw new JiraException("getSslConnectionSocketFactory", e);
        }
    }

    /**
     * Creates a new issue in the given project.
     *
     * @param project   Key of the project to create in
     * @param issueType Name of the issue type to create
     * @return a fluent create instance
     * @throws JiraException when something goes wrong
     */
    public Issue.FluentCreate createIssue(String project, String issueType) throws JiraException {
        return Issue.create(restclient, project, issueType);
    }

    /**
     * Retreives the issue with the given key.
     *
     * @param key Issue key (PROJECT-123)
     * @return an issue instance (issue includes all fields)
     * @throws JiraException when something goes wrong
     */
    public Issue getIssue(String key) throws JiraException {
        return Issue.get(restclient, key);
    }

    /**
     * Retreives the issue with the given key.
     *
     * @param key            Issue key (PROJECT-123)
     * @param includedFields Specifies which issue fields will be included in
     *                       the result.
     *                       <br>Some examples how this parameter works:
     *                       <ul>
     *                       <li>*all - include all fields</li>
     *                       <li>*navigable - include just navigable fields</li>
     *                       <li>summary,comment - include just the summary and comments</li>
     *                       <li>*all,-comment - include all fields</li>
     *                       </ul>
     * @return an issue instance
     * @throws JiraException when something goes wrong
     */
    public Issue getIssue(String key, String includedFields) throws JiraException {
        return Issue.get(restclient, key, includedFields);
    }

    /**
     * Retreives the issue with the given key.
     *
     * @param key            Issue key (PROJECT-123)
     * @param includedFields Specifies which issue fields will be included in
     *                       the result.
     *                       <br>Some examples how this parameter works:
     *                       <ul>
     *                       <li>*all - include all fields</li>
     *                       <li>*navigable - include just navigable fields</li>
     *                       <li>summary,comment - include just the summary and comments</li>
     *                       <li>*all,-comment - include all fields</li>
     *                       </ul>
     * @param expand         issue fields to expand when getting issue data
     * @return an issue instance
     * @throws JiraException when something goes wrong
     */
    public Issue getIssue(String key, String includedFields, String expand) throws JiraException {
        return Issue.get(restclient, key, includedFields, expand);
    }

    /**
     * count issues with the given query.
     *
     * @param jql JQL statement
     * @return the count
     * @throws JiraException when the search fails
     */
    public int countIssues(String jql) throws JiraException {
        return Issue.count(restclient, jql);
    }

    /**
     * Search for issues with the given query.
     *
     * @param jql JQL statement
     * @return a search result structure with results (issues include all
     * navigable fields)
     * @throws JiraException when the search fails
     */
    public Issue.SearchResult searchIssues(String jql) throws JiraException {
        return searchIssues(jql, null, null, null, null);
    }

    /**
     * Search for issues with the given query and max results.
     *
     * @param jql        JQL statement
     * @param maxResults limit the number of results
     * @return a search result structure with results (issues include all
     * navigable fields)
     * @throws JiraException when the search fails
     */
    public Issue.SearchResult searchIssues(String jql, Integer maxResults) throws JiraException {
        return searchIssues(jql, null, null, maxResults, null);
    }

    /**
     * Search for issues with the given query and specify which fields to
     * retrieve.
     *
     * @param jql            JQL statement
     * @param includedFields Specifies which issue fields will be included in
     *                       the result.
     *                       <br>Some examples how this parameter works:
     *                       <ul>
     *                       <li>*all - include all fields</li>
     *                       <li>*navigable - include just navigable fields</li>
     *                       <li>summary,comment - include just the summary and comments</li>
     *                       <li>*all,-comment - include all fields</li>
     *                       </ul>
     * @return a search result structure with results
     * @throws JiraException when the search fails
     */
    public Issue.SearchResult searchIssues(String jql, String includedFields) throws JiraException {
        return searchIssues(jql, includedFields, null, null, null);
    }

    /**
     * Search for issues with the given query and specify which fields to
     * retrieve and expand.
     *
     * @param jql            JQL statement
     * @param includedFields Specifies which issue fields will be included in
     *                       the result.
     *                       <br>Some examples how this parameter works:
     *                       <ul>
     *                       <li>*all - include all fields</li>
     *                       <li>*navigable - include just navigable fields</li>
     *                       <li>summary,comment - include just the summary and comments</li>
     *                       <li>*all,-comment - include all fields</li>
     *                       </ul>
     * @param expandFields   Specifies with issue fields should be expanded
     * @return a search result structure with results
     * @throws JiraException when the search fails
     */
    public Issue.SearchResult searchIssues(String jql, String includedFields, String expandFields) throws JiraException {
        return searchIssues(jql, includedFields, expandFields, null, null);
    }

    /**
     * Search for issues with the given query and specify which fields to
     * retrieve.
     *
     * @param jql            JQL statement
     * @param maxResults     limit the number of results
     * @param includedFields Specifies which issue fields will be included in
     *                       the result.
     *                       <br>Some examples how this parameter works:
     *                       <ul>
     *                       <li>*all - include all fields</li>
     *                       <li>*navigable - include just navigable fields</li>
     *                       <li>summary,comment - include just the summary and comments</li>
     *                       <li>*all,-comment - include all fields</li>
     *                       </ul>
     * @return a search result structure with results
     * @throws JiraException when the search fails
     */
    public Issue.SearchResult searchIssues(String jql, String includedFields, Integer maxResults) throws JiraException {
        return searchIssues(jql, includedFields, null, maxResults, null);
    }

    /**
     * Search for issues with the given query and specify which fields to
     * retrieve. If the total results is bigger than the maximum returned
     * results, then further calls can be made using different values for
     * the <code>startAt</code> field to obtain all the results.
     *
     * @param jql            JQL statement
     * @param includedFields Specifies which issue fields will be included in
     *                       the result.
     *                       <br>Some examples how this parameter works:
     *                       <ul>
     *                       <li>*all - include all fields</li>
     *                       <li>*navigable - include just navigable fields</li>
     *                       <li>summary,comment - include just the summary and comments</li>
     *                       <li>*all,-comment - include all fields</li>
     *                       </ul>
     * @param maxResults     if non-<code>null</code>, defines the maximum number of
     *                       results that can be returned
     * @param startAt        if non-<code>null</code>, defines the first issue to
     *                       return
     * @return a search result structure with results
     * @throws JiraException when the search fails
     */
    public Issue.SearchResult searchIssues(String jql, String includedFields, Integer maxResults, Integer startAt) throws JiraException {
        return searchIssues(jql, includedFields, null, maxResults, startAt);
    }

    /**
     * Search for issues with the given query and specify which fields to
     * retrieve. If the total results is bigger than the maximum returned
     * results, then further calls can be made using different values for
     * the <code>startAt</code> field to obtain all the results.
     *
     * @param jql            JQL statement
     * @param includedFields Specifies which issue fields will be included in
     *                       the result.
     *                       <br>Some examples how this parameter works:
     *                       <ul>
     *                       <li>*all - include all fields</li>
     *                       <li>*navigable - include just navigable fields</li>
     *                       <li>summary,comment - include just the summary and comments</li>
     *                       <li>-description - include navigable fields except the description (the default is *navigable for search)</li>
     *                       <li>*all,-comment - include everything except comments</li>
     *                       </ul>
     * @param expandFields   Specifies with issue fields should be expanded
     * @param maxResults     if non-<code>null</code>, defines the maximum number of
     *                       results that can be returned
     * @param startAt        if non-<code>null</code>, defines the first issue to
     *                       return
     * @return a search result structure with results
     * @throws JiraException when the search fails
     */
    public Issue.SearchResult searchIssues(String jql, String includedFields, String expandFields, Integer maxResults, Integer startAt) throws JiraException {
        return Issue.search(restclient, jql, includedFields, expandFields, maxResults, startAt);
    }

    /**
     * Retrieve the jira filter with the supplied id.
     *
     * @param id id of the filter to retreive.
     * @return The Jira filter with the supplied id
     * @throws JiraException
     */
    public Filter getFilter(final String id) throws JiraException {
        return Filter.get(restclient, id);
    }

    /**
     * @return a list of all priorities available in the Jira installation
     * @throws JiraException
     */
    public List<Priority> getPriorities() throws JiraException {
        try {
            URI uri = restclient.buildURI(Resource.getBaseUri() + "priority");
            JSON response = restclient.get(uri);
            JSONArray prioritiesArray = JSONArray.fromObject(response);

            List<Priority> priorities = new ArrayList<>(prioritiesArray.size());
            for (int i = 0; i < prioritiesArray.size(); i++) {
                JSONObject p = prioritiesArray.getJSONObject(i);
                priorities.add(new Priority(restclient, p));
            }

            return priorities;
        } catch (Exception ex) {
            throw new JiraException(ex.getMessage(), ex);
        }
    }

    /**
     * Get a list of options for a custom field
     *
     * @param field     field id
     * @param project   Key of the project context
     * @param issueType Name of the issue type
     * @return a search result structure with results
     * @throws JiraException when the search fails
     */
    public List<CustomFieldOption> getCustomFieldAllowedValues(String field, String project, String issueType) throws JiraException {
        JSONObject createMetadata = Issue.getCreateMetadata(restclient, project, issueType);
        JSONObject fieldMetadata = (JSONObject) createMetadata.get(field);
        return Field.getResourceArray(
                CustomFieldOption.class,
                fieldMetadata.get("allowedValues"),
                restclient
        );
    }

    /**
     * Get a list of options for a components
     *
     * @param project   Key of the project context
     * @param issueType Name of the issue type
     * @return a search result structure with results
     * @throws JiraException when the search fails
     */
    public List<Component> getComponentsAllowedValues(String project, String issueType) throws JiraException {
        JSONObject createMetadata = Issue.getCreateMetadata(restclient, project, issueType);
        JSONObject fieldMetadata = (JSONObject) createMetadata.get(Field.COMPONENTS);
        return Field.getResourceArray(
                Component.class,
                fieldMetadata.get("allowedValues"),
                restclient
        );
    }

    public RestClient getRestClient() {
        return restclient;
    }

    public String getSelf() {
        return username;
    }

    /**
     * Obtains the list of all projects in Jira.
     *
     * @return all projects; not all data is returned for each project; to get
     * the extra data use {@link #getProject(String)}
     * @throws JiraException failed to obtain the project list.
     */
    public List<Project> getProjects() throws JiraException {
        try {
            URI uri = restclient.buildURI(Resource.getBaseUri() + "project");
            JSON response = restclient.get(uri);
            JSONArray projectsArray = JSONArray.fromObject(response);

            List<Project> projects = new ArrayList<>(projectsArray.size());
            for (int i = 0; i < projectsArray.size(); i++) {
                JSONObject p = projectsArray.getJSONObject(i);
                projects.add(new Project(restclient, p));
            }

            return projects;
        } catch (Exception ex) {
            throw new JiraException(ex.getMessage(), ex);
        }
    }

    /**
     * Obtains information about a project, given its project key.
     *
     * @param key the project key
     * @return the project
     * @throws JiraException failed to obtain the project
     */
    public Project getProject(String key) throws JiraException {
        try {
            URI uri = restclient.buildURI(Resource.getBaseUri() + "project/" + key);
            JSON response = restclient.get(uri);
            return new Project(restclient, (JSONObject) response);
        } catch (Exception ex) {
            throw new JiraException(ex.getMessage(), ex);
        }
    }

    /**
     * Obtains the list of all issue types in Jira.
     *
     * @return all issue types
     * @throws JiraException failed to obtain the issue type list.
     */
    public List<IssueType> getIssueTypes() throws JiraException {
        try {
            URI uri = restclient.buildURI(Resource.getBaseUri() + "issuetype");
            JSON response = restclient.get(uri);
            JSONArray issueTypeArray = JSONArray.fromObject(response);

            List<IssueType> issueTypes = new ArrayList<>(issueTypeArray.size());
            for (int i = 0; i < issueTypeArray.size(); i++) {
                JSONObject it = issueTypeArray.getJSONObject(i);
                issueTypes.add(new IssueType(restclient, it));
            }

            return issueTypes;
        } catch (Exception ex) {
            throw new JiraException(ex.getMessage(), ex);
        }
    }

    /**
     * Creates a new component in the given project.
     *
     * @param project Key of the project to create in
     * @return a fluent create instance
     */
    public Component.FluentCreate createComponent(String project) {
        return Component.create(restclient, project);
    }

    /**
     * Obtains a component given its ID.
     *
     * @param id the component ID
     * @return the component
     * @throws JiraException failed to obtain the component
     */
    public Component getComponent(String id) throws JiraException {
        return Component.get(restclient, id);
    }

    public ArrayList<IssueHistory> filterChangeLog(List<IssueHistory> histoy, String fields) {
        ArrayList<IssueHistory> result = new ArrayList<>(histoy.size());
        fields = "," + fields + ",";

        for (IssueHistory record : histoy) {
            ArrayList<IssueHistoryItem> list = new ArrayList<>(record.getChanges().size());
            for (IssueHistoryItem item : record.getChanges()) {
                if (fields.contains(item.getField())) {
                    list.add(item);
                }
            }

            if (list.size() > 0) {
                result.add(new IssueHistory(record, list));
            }
        }
        return result;
    }

    public ArrayList<IssueHistory> getIssueChangeLog(Issue issue) throws JiraException {
        try {
            ArrayList<IssueHistory> changes = null;
            JSON response = getNextPortion(issue, 0);

            while (true) {
                JSONObject object = JSONObject.fromObject(response);
                Object opers = object.get("changelog");
                object = JSONObject.fromObject(opers);
                Integer totalObj = (Integer) object.get("total");
                JSONArray histories = JSONArray.fromObject(object.get("histories"));

                if (changes == null) {
                    changes = new ArrayList<>(totalObj);
                }

                for (int i = 0; i < histories.size(); i++) {
                    JSONObject p = histories.getJSONObject(i);
                    changes.add(new IssueHistory(restclient, p));
                }

                if (changes.size() >= totalObj) {
                    break;
                } else {
                    response = getNextPortion(issue, changes.size());
                }
            }

            return changes;
        } catch (Exception ex) {
            throw new JiraException(ex.getMessage(), ex);
        }
    }

    private JSON getNextPortion(Issue issue, Integer startAt) throws URISyntaxException, RestException, IOException {
        Map<String, String> params = new HashMap<>();
        if (startAt != null) {
            params.put("startAt", String.valueOf(startAt));
        }
        params.put("expand", "changelog.fields");
        URI uri = restclient.buildURI(Issue.getBaseUri() + "issue/" + issue.id, params);
        return restclient.get(uri);
    }
}
